###############################################################################
#                                                                             #
#    Copyright 2007 Danil Dotsenko
#                                                                             #
#    This program is free software; you can redistribute it and/or modify     #
#    it under the terms of the GNU General Public License as published by     #
#    the Free Software Foundation; either version 2 of the License.           #
#                                                                             #
#    This program is distributed in the hope that it will be useful,          #
#    but WITHOUT ANY WARRANTY; without even the implied warranty of           #
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the            #
#    GNU General Public License for more details.                             #
#                                                                             #
#    You should have received a copy of the GNU General Public License        #
#    along with this program; if not, write to the Free Software              #
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA #
#                                                                             #
###############################################################################
# Danil Dotsenko dd@accentsolution.com

import os
import flickrapi_dd
import re
from urllib import urlretrieve

# import the flickr module of your choice.
# you might need to do it like this: import plugins.FlickrModule
# because we are called from parent folder

print "loaded Flickr plugin"

class storageClass:
	'''
	This class helps to make the storage of temporary files easier.
	We will be storing ALL files for ALL instances of this plugin.
	'''
	def __init__(self, tempPath=None):
		if tempPath:
			self.tempPath = tempPath
		else:
			self.tempPath = '/tmp/kde-'+os.environ.get('USER')+'/afoto-flickr/'
		# this should look like /tmp/kde-username/afoto-flickr/
		if not os.path.isdir(self.tempPath):
			os.makedirs(self.tempPath)
			# think about permission settings.
	
	def addURL(self, url):
		'''
		Take a URL of a file, save it locally and return local path.
		If file is already found in cache, we don't download anything,
		and hapily return the path to local file.
		'''
		# 1. extract the file name out of the URL
		# 2. we will check the cache 
		# 3. and download image only if we don't have it.
		filename = os.path.basename(url)
		if os.path.isfile(self.tempPath+filename):
			return self.tempPath+filename
		else:
			try:
				localpath, messages = urlretrieve(url, self.tempPath+filename)
				return self.tempPath+filename
			except:
				return None
	
	def searchByURL(self, url):
		'''
		Testing if URL is already saved in cache. Here we accept raw image URLs
		and return local file name if file exists already.
		'''
		# 1. extract the file name out of the URL
		# 2. we will check the cache 
		filename = os.path.basename(url)
		if os.path.isfile(self.tempPath+filename):
			return self.tempPath+filename
		else:
			return None
	
	def searchLocal(self, snippet):
		'''
		Testing if text string matches any file already saved in cache.
		We return a list with local file names if files exist in storage.
		This may be useful for looking for file names incorporating IDs.
		Note: you may pass a regular expression to this formula, but
		we are matching on the filename ONLY, not the whole path.
		'''
		# 1. get full list of photos in the dir.
		# 2. find the snippet in the list.
		schObject = re.compile(snippet)
		filelist = []
		for root, dirs, files in os.walk(self.tempPath):
			for name in files:
				if schObject.search(name):
					filelist.append(os.path.join(root, name))
		return filelist

	def clean(self, mode='old', modeopts=24):
		'''
		Cleaning up the storage cache.
		Arguments:
		- mode can be one of "old", "size"
		- modeopts depends on mode.
		for "old" it's the time in hours from now.
		for "size" it's the maximum size of the folder in megabytes.
		'''
		# 1. get full list of photos in the dir.
		# 2. find the snippet in the list.
		schObject = re.compile(snippet)
		filelist = []
		for root, dirs, files in os.walk(self.tempPath):
			for name in files:
				if schObject.search(name):
					filelist.append(os.path.join(root, name))
		return filelist

class photoCollectionClass:
	'''
	This class contains the list of flick photo objects
	and allows to refresh, soft, randomize list; pick-one and 
	other methods of photo object manipulation.
	'''
	def __init__(self):
		'''
		'''
		self.photos = None # this will contain the flickapi photo objects.
		self.lastInternalURL = None
		self.lastPhotoID = None

	def __getOptions__(self, internalURL):
		# disassemble the testURL, extact keys into dict.
		options = {}
		for pair in internalURL.strip('flickr://|&').split('&'):
			o, v = pair.split('=')
			options[o] = v
		return options
		
	def refresh(self, internalURL=None):
		'''
		Repopulates the self.photos var with new flickr photo instances.
		Returns True if self.photos was changed.
		Note: We use Widget number as identifyer in multi-threaded use.
		'''
		success = False
		
		if internalURL:
			testURL = internalURL
		elif self.lastInternalURL:
			testURL = self.lastInternalURL
		else:
			testURL = None

		if testURL:
			options = self.__getOptions__(testURL)
			# this is going to be a long, winding road of picking and choosing the options
			# here is my picking priority:
			# 1. if interesting, group, or set are given - use them.
			# (at this time Image paths are not recognized by parser,
			#  so no worries about Image key)
			# 2. then, if tags are present but user ID is not, search for tags.
			# 3. lastly, it will be combinations of user + tags
			# 4. if we end up here, silently fail
			if options.has_key('interesting'):
				# at this time, this function's api does not support
				# additional options, like filtering by tag, or count limit
				photos = flickrapi_dd.interestingness()
			elif options.has_key('group'):
				# we don't handle tags with group yet
				photos = flickrapi_dd.Group(options['group']).getPhotos()
			elif options.has_key('set'):
				# I don't think this actually works properly.
				# also, we don't handle "tags" with "set" yet 
				photos = flickrapi_dd.Photoset(options['set']).getPhotos()
			elif options.has_key('tags') and not options.has_key('user'):
				photos = flickrapi_dd.photos_search(options['tags'].replace('+',','), tag_mode='all')
			elif options.has_key('user'):
				photos = flickrapi_dd.people_getPublicPhotos(options['user'])
			else:
				photos = []
			
			if photos:
				# even if the flickr call returns, it might not have found anything.
				self.photos = photos
				success = True

		return success
	
	def getNextPhoto(self, mode='gauss'):
		'''
		Returns a next flickr photo object.
		"Next" depends on softing and selection methods.
		Possible methods: 
		- gauss  (default) (aka 1/2 Bell-shaped - More hits from the beginning of the group.)
		- random (usuall "uniform" distribution)
		- most_recent (usually = 1st in line)
		- new ("Not stored in cache")
		Other fancy statistical sorting methods may come later.
		'''

		# for time-being, we will ignore the mode, and just assume "gauss"
		from random import gauss
		place = gauss(0,1).__abs__()
		# this is a "normal" distribution that I cut to posittive numbers only.
		# this means numbers closer to 0 occure more frequently.
		# for very small populations you will see a LOT of photos from the beginning of the set
		# if you start playing with flattening of distribution, make sure to change 
		# number 3 to something else, that describes your distribution edge better.
		if place > 3:
			place = 3
		# -0.5 is to improve rounding of int()
		place = int((self.photos.__len__() * (place / 3)) - 0.5)
		# if someone has time, check the max of formula above by hand.
		# if it generates out-of-range 
		
		try:
			return self.photos[place]
		except:
			return None

class pluginClass():
	'''
	Just random notes to myself. Real, helpfull comments will come later.
	'''
		
	def __init__(self):
		'''
		MANDATORY FUNCTION
		Init is run only ONCE, regardless of how many instance of this plugin
		are runing in all instances of afoto.
		Use this function to set and store variables that will be common for all consecutive
		instances and uses of this plugin.
		'''
		
		# MANDATORY VARIABLES
		self.filterStr = "^http://(www\.flickr|flickr)\.com/"
		#self.filterObj = None # this will be re.compile object, populated by main afoto code based on the filter above.
		self.filterObj = re.compile("^http://(www\.flickr|flickr)\.com/",re.IGNORECASE)
		self.protocol = 'flickr'
		# this refers to nice, internal, imagined URLS, like "flickr" or "asdf"
		# I am not sure how to use this yet,
		# I may instead populate this based on what .INI file says.
		# so, don't rely on / set protocol here just yet.
		# the value may vary depending on what INI file says when it calls this plugin.
		# and will likely be populated by main afoto code upon plugin initialization.

		# OPTIONAL VARIABLES
		self.flickr_API_key = '815a2a597d1c79de7f821bd4e3d35048'
		self.flickr_secret = 'c622194c5af38dd7'
		self.storage = storageClass()
		# storage is used by ALL threads, but uses one, common storage location
		self.collections = {}
		# collections can handle threads' settings in separate dict's.
		self.lastPhotoID = None

	def translatePath(self, url):
		'''
		MANDATORY FUNCTION
		This function converts real URLs into our internal, imaginary URLs
		'''
		# "explore/interesting/ 
		#    flickr.interestingness.getList(limit page number)
		# "search/?"
		#   - only parse the "q=" part for tags, nothing else. We don't even check for m=tags, but assume it is.
		#         you wanna be fancy? knock yourself out, implement different searching modes.
		# "groups/"
		#   - only get the pool photos. Get group ID from URL and go..
		# 		flickr.urls.lookupGroup
		#		flickr.groups.pools.getPhotos
		# "photos/"
		#   - if first thing that follows is /tags/ - user is NOT given
		#			use same thing as search/? above.
		#   - if first thing that follows is NOT /tags/, assume user is given
		#       - if /sets/ is present in string get SET pictures. (no need for user ID)
		#			get set number right out of the URL,
		#			flickr.photosets.getPhotos(setID)
		#       - if /tags/ is present in string get user's Tagged pictures.
		#			(not sure how yet, treat same as one bellow)
		#       - if NEITHER /sets/ nor /tags/ are present get ALL USER pictures.
		#			get user ID our of url, and pull photos.
		#			flickr.urls.lookupUser  ,  flickr.photos.search(userID, tags)
			
		newUrl = None
		
		try:
			keys = url[self.filterObj.search(url).group().__len__():]
		except:
			keys = None

		if keys:
			tags = []
			group = None
			user = None
			set = None
			image = None
			interesting = False
			if re.match('explore/interesting/', keys):
				interesting = True
			elif re.match('search/\?',keys):
				# there is only one thing we can get out of it - tags.
				# we can also treat them as keywords, but I really don't want to handle them as such.
				# looking for string surrounded by "(?or&)q=....&"
				# removing these
				# splitting into individual tags
				try:
					tags = re.search("[&|?]q=[^&]+&",keys).group().strip('?q=|&q=|&')
				except:
					pass
			elif re.match('groups/',keys):
				# there is only one thing that we can do with groups - get the name
				# use it to get the pool list.
				group = flickrapi_dd.urls_lookup_groupid(url)
			elif re.match('photos/tags/',keys):
				# collect the tag name.
				try:
					tags = re.search("(?<=/tags/)[^/]+(?=/)",keys).group()
				except:
					pass
			elif re.match('photos/', keys):
				# it would be hard to figure out what it is, just get a random foto for the user
				user = flickrapi_dd.urls_lookup_userid(url)
				try:
					tags = re.search("(?<=/tags/)[^/]+(?=/)",keys).group()
				except:
					pass
				try:
					set = re.search("(?<=/sets/)[^/]+(?=/)",keys).group()
				except:
					pass
			# assemble the new URL
			newUrl = 'flickr://'
			if interesting:
				newUrl = newUrl+'interesting='+str(int(interesting))+'&'
			if group:
				newUrl = newUrl+'group='+group+'&'
			if user:
				newUrl = newUrl+'user='+user+'&'
			if set:
				newUrl = newUrl+'set='+set+'&'
			if image:
				newUrl = newUrl+'image='+image+'&'
			if tags:
				newUrl = newUrl+'tags='+tags+'&'
		return newUrl

	def tryNextImage(self, widget, instance = {}, common= {}, customPath = None):
		'''
		MANDATORY FUNCTION
		This function prepares the image for consumption by setting a valid source to instance.cfg['currentPath']
		and changing instance.cfg['resourcePath'] in case when completely new source was dropped / chosen
		widget = widget mumber.
		common = pointer to appConfig.common a dictionary type with afoto-wide settings.
		instance = pointer to appConfig.instance[widget] a dictionary type with instance-specific settings.
		  instance.cfg['resourcePath'] already contains a proven, used source. 
		  if customPath is not usable or is not given, we will need to pull a new item out of instance.cfg['resourcePath']
		customPath = string (or unicode type) with new path we need to test.
		  (NOTE! It was already converted into our internal imaginary URL protocol by afoto's main code.
		  It used the translatePath() function.)
		  We test if that path is workable and try to pull a picture, If OK, we make it a new resourcePath
		'''
		
		# FOR PLUGIN WRITERS:
		# there are 4 things that have to happen in this function:
		# 0. if customPath is given, we translate the normal URL into our internal URL.
		#	  (main Afoto code already matched it to this plugin and translated it)
		# 1. test the path (new from customPath or old from instance.cfg['resourcePath'])
		# 2. if path is ok, pull up a picture to the local drive and set the address to the file to instance.cfg['currentPath']
		# 3. if we used customPath for the source, reset instance.cfg['resourcePath'] to the new address
		# 4. Change the Return value (success) to True if we really changed and prepared the image
		
		# NOTE! This function is called by all instances of afoto containing this plugin.
		# do not use constant names for temp files, of you will have a mess when more than one of this plugin is running.
		# add widget number to the file names to keep many instances of afoto separate.

		# Small plan of attack for flickr protocol.
		# - Create / Test a temp location for the image storage. (done in __init__)
		# - disassemble the path to understand variables, API id, username and other crap
		# - form a request
		# - Pull the image to temp
		# - Test that file has size. (not sure how to test that image IS image.)
		# - assign the right vars.
		# - Send success (no need to change any vars, if it's the same temp file.)

		success = False # This will mean: Nothing is changed, do not redraw

		if customPath:
			# it was dropped in or passed through URLOpen dialog and may be in raw format.
			# Again: It was already converted into our internal imaginary URL protocol by afoto's main code.
			internalURL = customPath
			print "Flickr plugin: testing new path ", internalURL
		else:
			# this means we are just reloading from the same, known location.
			internalURL = instance.cfg['resourcePath']
		# Example of an internalURL: flickr://user=96438615@N00&tags=portrait&
		
		if not self.collections.has_key(widget):
			# If settings for this widget do not exist yet, create them
			self.collections[widget]=photoCollectionClass()
			refresh = True
		else:
			# welcome repeat customers!
			# inset here formula for setting the refresh flag.
			# for example, based on skewed random number select False or True.
			# in the meantime - we refresh always
			refresh = True
		
		photo = None
		if refresh:
			if self.collections[widget].refresh(internalURL):
				photo = self.collections[widget].getNextPhoto()
		else:
			photo = self.collections[widget].getNextPhoto()

		if photo:
			# test if we are already shoing it.
			print "flickr plugin: almost there. We have a photo object. ", photo.id
			if not photo.id == self.collections[widget].lastPhotoID:
				localpath = self.storage.addURL(photo.getURL(urlType='source'))
				print "flickr plugin: Loading new image to: ", localpath
				# WARNING!!!!!
				# filename passed to SK MUST be a local path of form "/folder/folder/filename"
				# It cannot contain "file://"
				# I have seen local image FILES DISAPPEAR when superkaramba crashes because of
				# loading URL's with "file://"
				instance.cfg['currentPath'] = localpath
				if customPath:
					# meaning , if this path was dropped in / new.
					instance.cfg['resourcePath'] = customPath # note, it is already converted into internal URL.
				success = True

		return success